import time
import uuid
import hashlib
import json
import flask
import typing
import requests
import urllib
import argparse


class Blockchain:
    def __init__(self):
        self.chain = []
        self.currentTransactions = []
        self.nodes = []
        self.newBlock(proof=0, prevHash='0')

    def regNode(self, address: str) -> None:
        paresdURL = urllib.parse.urlparse(address)
        self.nodes.append(paresdURL.netloc)

    def validChain(self, chain: typing.List[typing.Dict[str, typing.Any]]) -> bool:
        lastBlock = chain[0]
        currentIndex = 1
        while currentIndex < len(chain):
            block = chain[currentIndex]
            if block['prevHash'] != self.hash(lastBlock):
                return False
            if not self.validProof(lastBlock['proof'], block['proof']):
                return False
            lastBlock = block
            currentIndex += 1
        return True

    def resolveConflicts(self) -> bool:
        neighbours = self.nodes
        newChain = None
        maxLength = len(self.chain)

        for node in neighbours:
            response = requests.get(f'http://{node}/chain')
            if response.status_code == 200:
                length = response.json()['length']
                chain = response.json()['chain']

                if length > maxLength and self.validChain(chain):
                    maxLength = length
                    newChain = chain

        if newChain:
            self.chain = newChain
            return True
        return False

    def newBlock(self, proof: int, prevHash: typing.Optional[str]) -> typing.Dict[str, typing.Any]:
        block = {
            'index': len(self.chain) + 1,
            'timestamp': time.time(),
            'transaction': self.currentTransactions,
            'proof': proof,
            'prevHash': prevHash or self.hash(self.lastBlock),
        }
        self.currentTransactions = []
        self.chain.append(block)
        return block

    def newTransaction(self, sender: str, recipient: str, amount: int) -> int:
        self.currentTransactions.append({
            'sender': sender,
            'recipient': recipient,
            'amount': amount
        })
        return self.lastBlock['index'] + 1

    @staticmethod
    def hash(block: typing.Dict[str, typing.Any]) -> str:
        blockStr = json.dumps(block, sort_keys=True).encode()
        return hashlib.sha256(blockStr).hexdigest()

    @property
    def lastBlock(self) -> typing.Dict[str, typing.Any]:
        return self.chain[-1]

    def proofOfWork(self, lastProof: int) -> int:
        proof = 0
        while self.validProof(lastProof, proof) is False:
            proof += 1
        return proof

    @staticmethod
    def validProof(lastProof: int, proof: int) -> bool:
        guess = f'{lastProof}{proof}'.encode()
        guessHash = hashlib.sha256(guess).hexdigest()
        return guessHash[0:4] == proofStr


app = flask.Flask(__name__)
blockchain = Blockchain()
nodeID = str(uuid.uuid4()).replace('-', '')


@app.route('/', methods=['GET'])
def index():
    return "hello"


@app.route('/transactions/new', methods=['POST'])
def newTransactions():
    values = flask.request.get_json()
    if values is None:
        response = {
            'message': 'Error: Missing Values'
        }
        return flask.jsonify(response), 400
    required = ['sender', 'recipient', 'amount']
    if not all(k in values for k in required):
        response = {
            'message': 'Error: Missing Values'
        }
        return flask.jsonify(response), 400
    index = blockchain.newTransaction(
        values['sender'], values['recipient'], values['amount'])
    response = {
        "message": f'Transcation will be added to Block{index}'
    }
    return flask.jsonify(response), 201


@app.route('/mine', methods=['GET'])
def mine():
    lastBlock = blockchain.lastBlock
    lastProof = lastBlock['proof']
    proof = blockchain.proofOfWork(lastProof)

    blockchain.newTransaction(
        sender="0",
        recipient=nodeID,
        amount=1
    )
    block = blockchain.newBlock(proof, None)
    response = {
        "message": "New Block Forged",
        "index": block['index'],
        "transaction": block['transaction'],
        "proof": block['proof'],
        "prevHash": block['prevHash']
    }

    return flask.jsonify(response), 200


@app.route('/chain', methods=['GET'])
def chain():
    response = {
        'chain': blockchain.chain,
        'length': len(blockchain.chain)
    }
    return flask.jsonify(response)


@app.route('/nodes/register', methods=['POST'])
def regNodes():
    values = flask.request.get_json()

    nodes = values.get('nodes')
    if nodes is None:
        response = {
            'message': 'Error: Please supply a valid list of nodes'
        }
        return flask.jsonify(response), 400
    for node in nodes:
        blockchain.regNode(node)
    response = {
        'message': 'New nodes have been added',
        'totalNodes': list(blockchain.nodes),
    }
    return flask.jsonify(response), 201


@app.route('/nodes/resolve', methods=['GET'])
def consensus():
    replaced = blockchain.resolveConflicts()
    if replaced:
        response = {
            'message': 'Our chain was replaced',
            'newChain': blockchain.chain
        }
    else:
        response = {
            'message': 'Our chain is authoritative',
            'chain': blockchain.chain
        }
    return flask.jsonify(response), 200


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('-p', '--port',
                        default=5000,
                        type=int,
                        help='select port to listen on')
    parser.add_argument('-f', '--proof',
                        default=4,
                        type=int,
                        help='change work of proof')
    args = parser.parse_args()
    port = args.port
    proofLength = args.proof
    tempStr = "0000000000000000000000000000000000000000000000000000000000000000"
    proofStr = tempStr[:proofLength]
    app.run(host='0.0.0.0', port=port)
